package io.noties.markwon.base

import io.noties.markwon.plugin.callout.model.CallOutNode
import org.commonmark.node.BlockQuote
import org.commonmark.node.BulletList
import org.commonmark.node.Code
import org.commonmark.node.CustomBlock
import org.commonmark.node.CustomNode
import org.commonmark.node.Document
import org.commonmark.node.Emphasis
import org.commonmark.node.FencedCodeBlock
import org.commonmark.node.HardLineBreak
import org.commonmark.node.Heading
import org.commonmark.node.HtmlBlock
import org.commonmark.node.HtmlInline
import org.commonmark.node.Image
import org.commonmark.node.IndentedCodeBlock
import org.commonmark.node.Link
import org.commonmark.node.LinkReferenceDefinition
import org.commonmark.node.ListItem
import org.commonmark.node.Node
import org.commonmark.node.OrderedList
import org.commonmark.node.Paragraph
import org.commonmark.node.SoftLineBreak
import org.commonmark.node.StrongEmphasis
import org.commonmark.node.Text
import org.commonmark.node.ThematicBreak
import org.commonmark.node.Visitor
import timber.log.Timber
import java.util.HashMap
import java.util.Locale

/**
 * Configurable visitor of parsed markdown. Allows visiting certain (registered) nodes without
 * need to create own instance of this class.
 *
 * @see MarkwonVisitor.Builder.on
 * @see //configureVisitor
 * @since 3.0.0
 */
interface MarkwonVisitor : Visitor {
    /**
     * @see MarkwonVisitor.Builder.on
     */
    interface NodeVisitor<N : Node> {
        fun visit(visitor: MarkwonVisitor, n: N)
    }

    /**
     * Primary purpose is to control the spacing applied before/after certain blocks, which
     * visitors are created elsewhere
     *
     * @since 4.3.0
     */
    interface BlockHandler {
        fun blockStart(visitor: MarkwonVisitor, node: Node)
        fun blockEnd(visitor: MarkwonVisitor, node: Node)
    }

    interface Builder {
        /**
         * @param node        to register
         * @param nodeVisitor [MarkwonVisitor.NodeVisitor] to be used or null to ignore previously registered
         * visitor for this node
         */
        fun <N : Node> on(node: Class<N>, nodeVisitor: NodeVisitor<in N>?): Builder
        //fun <N : Node> on(node: Class<N>, nodeVisitor: NodeVisitor<N>): Builder

        fun <N : Node> on(node: Class<N>, nodeVisitor: (visitor: MarkwonVisitor, node: N)->Unit): Builder{
           return  on(node,nodeVisitor=object:NodeVisitor<N>{
                override fun visit(visitor: MarkwonVisitor, node: N) {
                    nodeVisitor(visitor, node)
                }
            })
        }

        /**
         * @param blockHandler to handle block start/end
         * @see io.noties.markwon.base.ext.MarkwonVisitor.BlockHandler
         *
         * @see BlockHandlerDef
         *
         * @since 4.3.0
         */
        fun blockHandler(blockHandler: BlockHandler): Builder

        fun build(configuration: MarkwonConfiguration, renderProps: RenderProps): MarkwonVisitor
    }

    fun configuration(): MarkwonConfiguration
    fun renderProps(): RenderProps
    fun builder(): SpannableBuilder

    /**
     * Visits all children of supplied node.
     *
     * @param node to visit
     */
    fun visitChildren(node: Node)

    /**
     * Executes a check if there is further content available.
     *
     * @param node to check
     * @return boolean indicating if there are more nodes after supplied one
     */
    fun hasNext(node: Node): Boolean

    /**
     * This method **ensures** that further content will start at a new line. If current
     * last character is already a new line, then it won\'t do anything.
     */
    fun ensureNewLine()

    /**
     * This method inserts a new line without any condition checking (unlike [.ensureNewLine]).
     */
    fun forceNewLine()

    /**
     * Helper method to call `builder().length()`
     *
     * @return current length of underlying [SpannableBuilder]
     */
    fun length(): Int

    /**
     * Clears state of visitor (both [RenderProps] and [SpannableBuilder] will be cleared
     */
    fun clear()

    /**
     * Sets `spans` to underlying [SpannableBuilder] from *start*
     * to *[SpannableBuilder.length]*.
     *
     * @param start start position of spans
     * @param spans to apply
     */
    fun setSpans(start: Int, spans: Any)

    /**
     * Helper method to obtain and apply spans for supplied Node. Internally queries [io.noties.markwon.base.SpanFactory]
     * for the node (via [io.noties.markwon.base.MarkwonSpansFactory.require] thus throwing an exception
     * if there is no [io.noties.markwon.base.SpanFactory] registered for the node).
     *
     * @param node  to retrieve [io.noties.markwon.base.SpanFactory] for
     * @param start start position for further [.setSpans] call
     * @see .setSpansForNodeOptional
     */
    fun <N : Node> setSpansForNode(node: N, start: Int)

    /**
     * The same as [.setSpansForNode] but can be used in situations when there is
     * no access to a Node instance (for example in HTML rendering which doesn\'t have markdown Nodes).
     *
     * @see .setSpansForNode
     */
    fun <N : Node> setSpansForNode(node: Class<N>, start: Int)
    // does not throw if there is no SpanFactory registered for this node
    /**
     * Helper method to apply spans from a [io.noties.markwon.base.SpanFactory] **if** it\'s registered in
     * [io.noties.markwon.base.MarkwonSpansFactory] instance. Otherwise ignores this call (no spans will be applied).
     * If there is a need to ensure that specified `node` has a [SpanFactory] registered,
     * then [.setSpansForNode] can be used. [.setSpansForNode] internally
     * uses [io.noties.markwon.base.MarkwonSpansFactory.require]. This method uses [MarkwonSpansFactory.get].
     *
     * @see .setSpansForNode
     */
    fun <N : Node> setSpansForNodeOptional(node: N, start: Int)

    /**
     * The same as [.setSpansForNodeOptional] but can be used in situations when
     * there is no access to a Node instance (for example in HTML rendering).
     *
     * @see .setSpansForNodeOptional
     */
    fun <N : Node> setSpansForNodeOptional(node: Class<N>, start: Int)

    /**
     * @since 4.3.0
     */
    fun blockStart(node: Node)

    /**
     * @since 4.3.0
     */
    fun blockEnd(node: Node)
}

/**
 * @since 3.0.0
 */
internal class MarkwonVisitorImpl(
    private val configuration: MarkwonConfiguration,
    private val renderProps: RenderProps,
    private val builder: SpannableBuilder,
    private val nodes: Map<Class<out Node>, MarkwonVisitor.NodeVisitor<out Node>>,
    // @since 4.3.0
    private val blockHandler: MarkwonVisitor.BlockHandler
) : MarkwonVisitor {
    val callOutPattern="\\[!\\w*\\]".toRegex() //[!INFO]

    override fun visit(blockQuote: BlockQuote) {
        val firstLine = blockQuote.firstChild?.firstChild
        if(firstLine is Text) {
           callOutPattern.matchAt(firstLine.literal.trimStart(),0)?.let { matchResult ->
               val type = matchResult.value.lowercase(Locale.getDefault()).subSequence(2,matchResult.value.length-1)
               val callOutNode = CallOutNode.build(type.toString())
               Timber.d("callOut Pattern match ${callOutNode.type}")
               configuration.blockQuoteCallOutMap[type]?.invoke(blockQuote,callOutNode)?.let {
                   firstLine.literal = firstLine.literal.replace(matchResult.value,"")
                   //blockQuote.prependChild(callOutNode)
                   visitNode(it)
                   return
               }
           }
        }
        visitNode( blockQuote )
    }

    override fun visit(bulletList: BulletList) {
        visitNode(bulletList )
    }

    override fun visit(code: Code) {
        visitNode(code )
    }

    override fun visit(document: Document) {
        visitNode(document )
    }

    override fun visit(emphasis: Emphasis) {
        visitNode(emphasis )
    }

    override fun visit(strongEmphasis: StrongEmphasis) {
        visitNode(strongEmphasis )
    }

    override fun visit(fencedCodeBlock: FencedCodeBlock) {
        val newBlock  = configuration.fencedCodeBlockMap[fencedCodeBlock.info]?.invoke(fencedCodeBlock)?:fencedCodeBlock
        visitNode( newBlock )
    }

    override fun visit(hardLineBreak: HardLineBreak) {
        visitNode(hardLineBreak )
    }

    override fun visit(heading: Heading) {
        visitNode(heading )
    }

    override fun visit(thematicBreak: ThematicBreak) {
        visitNode(thematicBreak )
    }

    override fun visit(htmlInline: HtmlInline) {
        visitNode(htmlInline )
    }

    override fun visit(htmlBlock: HtmlBlock) {
        visitNode(htmlBlock )
    }

    override fun visit(image: Image) {
        visitNode(image )
    }

    override fun visit(indentedCodeBlock: IndentedCodeBlock) {
        visitNode(indentedCodeBlock )
    }

    override fun visit(link: Link) {
        visitNode(link )
    }

    override fun visit(listItem: ListItem) {
        visitNode(listItem )
    }

    override fun visit(orderedList: OrderedList) {
        visitNode(orderedList )
    }

    override fun visit(paragraph: Paragraph) {
        visitNode(paragraph )
    }

    override fun visit(softLineBreak: SoftLineBreak) {
        visitNode(softLineBreak )
    }

    override fun visit(text: Text) {
        visitNode(text )
    }

    override fun visit(linkReferenceDefinition: LinkReferenceDefinition) {
        visitNode(linkReferenceDefinition )
    }

    override fun visit(customBlock: CustomBlock) {
        visitNode(customBlock )
    }

    override fun visit(customNode: CustomNode) {
        visitNode(customNode )
    }


    override fun configuration(): MarkwonConfiguration {
        return configuration
    }

    override fun renderProps(): RenderProps {
        return renderProps
    }

    override fun builder(): SpannableBuilder {
        return builder
    }

    private  fun < T:Node> visitNode(node: T) {
        val nodeVisitor = nodes[node.javaClass] as MarkwonVisitor.NodeVisitor<T>?
        val theme=configuration.categoryTheme()
        if (nodeVisitor != null) {
            theme.nodeNext(node.javaClass.name)
            nodeVisitor.visit(this, node)
        } else {
            theme.nodeIn(node.javaClass.name)
            visitChildren(node)
            theme.nodeOut(node.javaClass.name)
        }
    }

    override fun visitChildren(parent: Node) {
        var node = parent.firstChild
        while (node != null) {
            // A subclass of this visitor might modify the node, resulting in getNext returning a different node or no
            // node after visiting it. So get the next node before visiting.
            val next = node.next
            node.accept(this)
            node = next
        }
    }

    override fun hasNext(node: Node): Boolean {
        return node.next != null
    }

    override fun ensureNewLine() {
        if (builder.isNotEmpty() && '\n' != builder.lastChar()) {
            builder.append('\n')
        }
    }

    override fun forceNewLine() {
        builder.append('\n')
    }

    override fun length(): Int {
        return builder.length
    }

    override fun setSpans(start: Int, spans: Any) {
        SpannableBuilder.setSpans(builder, spans, start, builder.length)
    }

    override fun clear() {
        renderProps.clearAll()
        builder.clear()
    }

    override fun <N : Node> setSpansForNode(node: N, start: Int) {
        setSpansForNode(node.javaClass, start)
    }

    override fun <N : Node> setSpansForNode(node: Class<N>, start: Int) {
        setSpans(
            start,
            configuration.spansFactory().require(node).getSpans(configuration, renderProps)
        )
    }

    override fun <N : Node> setSpansForNodeOptional(node: N, start: Int) {
        setSpansForNodeOptional(node.javaClass, start)
    }

    override fun <N : Node> setSpansForNodeOptional(node: Class<N>, start: Int) {
        val factory = configuration.spansFactory()[node]
        if (factory != null) {
            setSpans(start, factory.getSpans(configuration, renderProps))
        }
    }

    override fun blockStart(node: Node) {
        blockHandler.blockStart(this, node)
    }

    override fun blockEnd(node: Node) {
        blockHandler.blockEnd(this, node)
    }

    internal class BuilderImpl : MarkwonVisitor.Builder {
        private val nodes: MutableMap<Class<out Node>, MarkwonVisitor.NodeVisitor<out Node>> = HashMap()
        private var blockHandler: MarkwonVisitor.BlockHandler? = null

        override fun <N : Node> on(
            node: Class<N>,
            nodeVisitor: MarkwonVisitor.NodeVisitor<in N>?
        ): MarkwonVisitor.Builder {

            // @since 4.1.1 we might actually introduce a local flag to check if it's been built
            //  and throw an exception here if some modification is requested
            //  NB, as we might be built from different threads this flag must be synchronized

            // we should allow `null` to exclude node from being visited (for example to disable
            // some functionality)
            if (nodeVisitor == null) {
                nodes.remove(node)
            } else {
                nodes[node] = nodeVisitor
            }
            return this
        }


        override fun blockHandler(blockHandler: MarkwonVisitor.BlockHandler): MarkwonVisitor.Builder {
            this.blockHandler = blockHandler
            return this
        }

        override fun build(
            configuration: MarkwonConfiguration,
            renderProps: RenderProps
        ): MarkwonVisitor {
            // @since 4.3.0
            return MarkwonVisitorImpl(
                configuration,
                renderProps,
                SpannableBuilder(),
                nodes,
                blockHandler?:BlockHandlerDef()
            )
        }
    }
}